(function(global) {

	var events = 'Uninitialized Loading Loaded Interactive Complete'.split(' '),
		hasOwn = Object.prototype.hasOwnProperty;
	function emptyFunction() {}
	// http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context
	function globalEval(src) {
		if (global.execScript) {
			return global.execScript(src);
		}
		(function() {
			global.eval.call(global, src);
		})();
	}

	// example events firing order:
	// 200: onLoading, onLoaded, [onInteractive], onComplete, on200, onSuccess
	// 404: onLoading, onLoaded, [onInteractive], onComplete, on404, onFailure
	// Network error: onUninitialized, onComplete, onNetworkError, onFailure
	function ajax(url, opt) {
		if (Object.prototype.toString.call(url) != '[object String]') {
			opt = url;
			url = opt.url || '';
		}
		var p, rh;
		opt = opt || {};
		for (p in ajax.defaultOptions) {
			if (!hasOwn.call(ajax.defaultOptions, p) || p in opt) {
				continue;
			}
			if (p == 'requestHeader') {
				opt[p] = opt[p] || {};
				for (rh in ajax.defaultOptions[p]) {
					if (!hasOwn.call(ajax.defaultOptions[p], rh) || rh in opt[p]) {
						continue;
					}
					opt[p][rh] = ajax.defaultOptions[p][rh];
				}
			}
			opt[p] = ajax.defaultOptions[p];
		}

		var xhr = ajax.getXhr();
		function notify(name) {
			name = 'on' + name;
			if (typeof opt.onDebug == 'function') {
				opt.onDebug.call(xhr, name, opt);
			}
			if (typeof opt[name] == 'function') {
				opt[name].call(xhr, opt);
			}
		}
		// decide if we are file or http or https
		var pcol = (url+'').match(/^([a-z]+)?s?\:/i);
		opt.isHttp = (pcol ? pcol[1] == 'http' : global.location.protocol.match(/^https?\:/));
		var timeout = global.setTimeout(function() {
			xhr.abort();
		}, opt.timeout * 1000);
		// serialize form if needed
		if (opt.form) {
			var form = (typeof opt.form == 'string' ? document.getElementById(opt.form) : opt.form);
			opt.parameters = ajax.serializeForm(form);
			if (form.enctype) {
				opt.requestHeaders['Content-type'] = form.enctype;
			}
			if (form.method) {
				opt.method = form.method;
			}
			url = form.action || url;
		}
		else if (opt.method == 'POST') {
			opt.requestHeaders['Content-type'] = opts.contentType + (opt.encoding ? '; charset=' + opt.encoding: '');
		}
		// POST/GET is not case sensitive
		opt.method = opt.method.toUpperCase();
		opt.url = url;
		// convert params object to string if needed
		if (Object.prototype.toString.call(opt.parameters) != '[object String]') {
			opt.parameters = ajax.objectToParams(opt.parameters);
		}
		// add parameters to url if the metiond is GET
		if (opt.parameters.length && opt.method == 'GET') {
			url += (url.indexOf('?') > -1 ? '&' : '?') + opt.parameters;
		}
		// open request
		xhr.open(opt.method, url, opt.async, opt.username, opt.password);
		// define callback
		xhr.onreadystatechange = function() {
			var state = xhr.readyState, status;
			if (state == 4) {
				global.clearTimeout(timeout);
				xhr.onreadystatechange = emptyFunction;
				try {
					status = xhr.status;
					status = status == 1223 ? 204 : status;
				}
				catch (e) {
					status = 13030;
				}
				// non-http requests fire 0 regardless of success or failure
				if (!opt.isHttp && status === 0) {
					// define success on non-http requests as something with response text
					status = (xhr.responseText || '').length ? 200 : 404;
				}
				// network error timeout status codes: normalize to onNetworkError
				// from "Ajax: the Complete Reference" by Thomas Powell - http://books.google.com/books?id=p9hrjVMAeYkC&pg=PA180
				// 12002: internet timeout
				// 12007: network error
				// 12029: dropped connection
				// 12030: dropped connection
				// 12031: dropped connection
				// 12152: connection closed by server
				// 13030: exception code (Browser threw exception when attempting to read status)
				if (' 12002 12007 12029 12030 12031 12152 13030 0 '.indexOf(' ' + status + ' ') > -1) {
					notify('NetworkError');
					status = 0;
				}
				xhr.isSuccess = status >= 200 && status < 300;
				if (!xhr.isSuccess) {
					if (status > 0) {
						notify(status);
					}
					notify('Failure');
					return;
				}

				// options for special actions
				var contentType = xhr.getResponseHeader('Content-type');
				// update an element with response html
				if (opt.element) {
					var element = (typeof opt.element == 'string' ? document.getElementById(opt.element) : opt.element);
					element.innerHTML = xhr.responseText;
					if (opt.evalJS) {
						ajax.evalScripts(xhr.responseText);
					}
				}
				// turn JSON text into JavaScript object
				else if (opt.evalJSON && (opt.json || (/^(application|text)\/(x-)?json(request)?/).test(contentType)) && !xhr.responseJSON) {
					xhr.responseJSON = ajax.evalJSON(xhr.responseText);
				}
				// eval JS files
				else if (opt.evalJS && (opt.js || (/^(application|text)\/javascript/).test(contentType))) {
					globalEval(xhr.responseText);
				}
				// turn XML text into DOM Document Object
				else if (opt.xml || (/^(application|text)\/xml/).test(contentType) && !xhr.responseXML) {
					xhr.responseXML = ajax.getXmlDoc(xhr.responseText);
				}
				notify('Complete');
				notify(status);
				notify('Success');
			}
			else {
				// end status == 4
				notify(events[state]); // onUninitialized, onLoading, onLoaded, onInteractive
			}
		}; // end onreadsystatechange

		// set request headers
		for (p in opt.requestHeaders) {
			if (!hasOwn.call(opt, p)) {
				continue;
			}
			xhr.setRequestHeader(p, opt.requestHeaders[p]);
		}
		xhr.send(opt.method == 'POST' ? opt.parameters : undefined);
		xhr.requestOptions = opt;
		return xhr;
	};
	// end way-to-long function :)
	ajax.evalScripts = function evalScripts(html) {
		html.replace((/<script[^>]*>([\\S\\s]*?)<\/script>/g), function(m) {
			globalEval(m[1]);
		});
	};
	ajax.evalJSON = function evalJSON(json) {
		var toCheck = json.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
		if ((/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(toCheck)) {
			return eval("(" + json + ")");
		}
		return false;
	};
	ajax.getXmlDoc = function getXmlDoc(xml) {
		var doc;
		if (global.DOMParser) {
			//EOMB
			doc = new DOMParser().parseFromString(xml, "text/xml");
		}
		else {
			//Internet Explorer
			doc = new ActiveXObject("Microsoft.XMLDOM");
			doc.async = false;
			doc.loadXML(xml);
		}
		return doc;
	};

	ajax.defaultOptions = {
		url: false,
		method: 'post',
		async: true,
		contentType: 'application/x-www-form-urlencoded',
		encoding: 'UTF-8',
		parameters: '',
		timeout: 30, // number of seconds after which to abort the request
		requestHeaders: {
			'X-Requested-With': 'XMLHttpRequest',
			'User-Agent': 'XMLHTTP/1.0',
			'Accept': 'text/javascript, text/html, application/xml, text/xml, text/json, application/json, *' + '/' + '*'
		},
		element: false,
		form: false,
		json: false,
		evalJSON: true,
		evalJS: true
	};
	ajax.getXhr = function getTransport() {
		try {return new XMLHttpRequest();} catch(e) {}
		try {return new ActiveXObject('Msxml3.XMLHTTP');} catch(e) {}
		try {return new ActiveXObject('Msxml2.XMLHTTP');} catch(e) {}
		try {return new ActiveXObject('Microsoft.XMLHTTP');} catch(e) {}
		throw 'Browser does not support ajax.';
	};
	ajax.serializeForm = function serializeForm(form) {
		form = (typeof form == 'string' ? document.getElementById(form) : form);
		var el, item = 0, i, name, selected, opt, value, data = {};

		// Iterate over the form elements collection to construct the
		// label-value pairs.
		while ((el = form.elements[item++])) {
			// Do not submit fields that are disabled or
			// do not have a name attribute value.
			if (el.disabled == false && (name = el.name)) {
				switch (el.type) {
					// Safari, Opera, FF all default opt.value from .text if
					// value attribute not specified in markup
					case 'select':
					case 'select-one':
						if (el.selectedIndex > -1) {
							opt = el.options[el.selectedIndex];
							value = (opt.attributes.value && opt.attributes.value.specified ? opt.value : opt.text);
						}
						break;
					case 'select-multiple':
						selected = [];
						i = 0;
						while ((opt = el.options[i++])) {
							if (opt.selected) {
								selected.push(opt.attributes.value && opt.attributes.value.specified ? opt.value : opt.text);
							}
						}
						value = selected;
						break;
					case 'radio':
					case 'checkbox':
						if (el.checked) {
							value = el.value;
						}
						else {
							continue;
						}
						break;
					case 'file':
						// stub case as XMLHttpRequest will only send the file path as a string.
					case undefined:
						// stub case for fieldset element which returns undefined.
					case 'reset':
						// stub case for input type reset button.
					case 'button':
						// stub case for input type button elements.
						break;
					case 'submit':
						// TODO: determine which button was clicked??
	//            if (hasSubmit === false) {
	//              if (this._hasSubmitListener && this._submitElementValue){
	//                data[item++] = this._submitElementValue;
	//              } else {
	//                data[item++] = oName + oValue;
	//              }
	//              hasSubmit = true;
	//            }
	//            break;

					default:
						value = el.value;
				}
				if (Object.prototype.toString.call(data[name]) == '[object Array]') {
					data[name].push(value);
				}
				else if (name in data) {
					data[name] = [data[name], value];
				}
				else {
					data[name] = value;
				}
			}
		}
		return data;
	};

	ajax.objectToParams = function objectToParams(object, delimiter, glue) {
		var pairs = [], key, value, escValue, i, len;
		glue = glue || '=';
		for (var prop in object) {
			if (!hasOwn.call(object, prop)) {
				continue;
			}
			key = encodeURIComponent(prop);
			value = object[prop];
			if (Object.prototype.toString.call(value) == '[object Array]') {
				if (value.length) {
					for (i = 0, len = value.length; i < len; i++) {
						escValue = encodeURIComponent(typeof value[i] == 'undefined' || value[i] === null ? false : value[i]);
						pairs.push(key + '[]' + (escValue ? glue + escValue : ''));
					}
				}
				else {
					pairs.push(key + '[]');
				}
			}
			else {
				escValue = typeof value == 'undefined' || value === null ? false : encodeURIComponent(value);
				pairs.push(key + (escValue ? glue + escValue : ''));
			}
		}
		return pairs.join(delimiter || '&');
	};

	// create ProtoLove if not exists
	global.ProtoLove = global.ProtoLove || {};
	// export ajax
	global.ProtoLove.ajax = ajax;
	global.ajax = global.ajax || ajax;

})(this);
